P02-1. 1세대 Sidecar 메시의 커널 내부 해부 - 이론 개념

Part 02 (이론): 1세대 Sidecar 메시의 리눅스 커널 내부 해부 (Webhook · iptables · UID 1337)

핵심 학습 목표

  1. 내가 작성한 YAML에는 컨테이너가 1개뿐인데, 파드가 생성될 때 어떻게 istio-init 과 istio-proxy 컨테이너가 자동으로 끼워 넣어지는지(MutatingAdmissionWebhook) 설명할 수 있다.
  2. 초기화 컨테이너(istio-init)가 왜 본 컨테이너들보다 먼저 실행되어야 하며, 왜 NET_ADMIN / NET_RAW 권한을 필요로 하는지 이해한다.
  3. 리눅스 커널의 패킷 검문소인 Netfilter / iptables (nat 테이블) 이 어떻게 나가는 패킷을 15001번으로, 들어오는 패킷을 15006번으로 투명하게 납치(REDIRECT)하는지 그림으로 설명할 수 있다.
  4. 패킷이 파드 안에서 영원히 맴도는 무한 루프(Infinite Loop) 를 막기 위해 Istio가 사용하는 두 가지 핵심 기법(UID 1337 예외 처리와 127.0.0.6 출발지 루프백 마법)을 완벽히 해부한다.
  5. 1세대 사이드카 구조의 태생적 한계 4가지를 짚어내고, 왜 2세대 Ambient Mesh(Part 03) 가 탄생할 수밖에 없었는지 연결한다.

Part 01에서 남았던 두 가지 미스터리

앞선 Part 01 실습에서 우리는 두 가지 신기한 현상을 목격했습니다.

  • 미스터리 ① (파드 개조): 분명히 order-api.yaml 파일에는 컨테이너를 1개만 적었고 파일 내용을 한 글자도 고치지 않았는데, 네임스페이스에 istio-injection=enabled 라벨을 붙이고 파드를 다시 띄우자 갑자기 READY 2/2가 되었습니다. 도대체 누가 내 YAML 설계도를 중간에서 몰래 고쳐 놓은 걸까요?
  • 미스터리 ② (패킷 납치): 내 curl 프로그램은 프록시 설정(HTTP_PROXY 환경변수 등)을 전혀 하지 않고 그냥 평소처럼 http://order-api:80으로 패킷을 던졌는데, 어느새 옆자리의 Envoy 프록시(15001번 창구)가 그 패킷을 가로채서 처리했습니다. 도대체 리눅스 커널 안에서 누가 패킷의 멱살을 잡아 Envoy에게 끌고 간 걸까요?

이번 Part 02에서는 이 두 가지 마법의 정체를 쿠버네티스 API 서버와 리눅스 커널 수준까지 내려가 낱낱이 파헤칩니다.


1. 미스터리 ① 해부: 누가 내 파드에 사이드카를 끼워 넣었나? (MutatingAdmissionWebhook)

1-1. 쿠버네티스 API 서버(kube-apiserver)의 4단계 입국 심사대

우리가 터미널에서 kubectl apply -f order-api.yaml을 치면, 그 YAML 설계도는 곧바로 클러스터 장부(etcd)에 저장되는 것이 아닙니다. kube-apiserver라는 본사 민원 창구 안에 있는 4단계 입국 심사대를 순서대로 모두 통과해야만 비로소 etcd 장부에 기록됩니다.

flowchart LR
    U["👩‍💻 kubectl apply
(컨테이너 1개짜리
순수 Pod YAML)"] --> AUTH["1️⃣ 인증
(Authentication)
'누구십니까?'"] AUTH --> RBAC["2️⃣ 인가
(Authorization / RBAC)
'파드 만들 권한 있습니까?'"] RBAC --> MUT["3️⃣ 변형 어드미션 웹훅
(Mutating Webhook)
🔧 '설계도 개조 수술!'"] MUT --> VAL["4️⃣ 검증 어드미션 웹훅
(Validating Webhook)
🔍 '개조된 최종본 합격 검사!'"] VAL --> ETCD["5️⃣ etcd 저장
(컨테이너 2개 +
초기화 컨테이너 1개)"] ISTIOD["🧠 istiod
(istio-sidecar-injector)
:443 웹훅 서버"] MUT -- "HTTPS 요청:
'이 Pod 개조해 주세요'" --> ISTIOD ISTIOD -- "JSONPatch 응답:
'istio-init 과 istio-proxy
2개를 추가하세요!'" --> MUT

각 심사대가 하는 일을 건물 건축 허가 과정에 비유해 보겠습니다.

단계 심사대 이름 건축 허가 비유 쿠버네티스에서의 실제 역할
1 인증 (Authentication) 신청자의 주민등록증 확인 kubeconfig의 인증서나 토큰을 보고 "이 요청을 보낸 사람이 유효한 사용자인가?" 확인
2 인가 (Authorization / RBAC) 신청자가 그 땅에 건물을 지을 자격(권한)이 있는지 확인 "이 사용자가 part1-demo 네임스페이스에 Pod를 create할 권한이 있는가?" 확인
3 변형 어드미션 웹훅 (MutatingAdmissionWebhook) 소방서 직원이 도면을 보더니 "스프링클러(istio-proxy)와 배관(istio-init)을 도면에 자동으로 그려 넣어 주는 단계" 저장되기 직전에 YAML 내용을 가로채서 필드를 추가하거나 수정(Mutate) 하는 단계 ← 사이드카가 여기서 들어갑니다!
4 검증 어드미션 웹훅 (ValidatingAdmissionWebhook) 스프링클러까지 다 그려진 최종 도면이 건축법에 맞는지 최종 도장 찍기 수정이 끝난 최종 YAML이 규정에 맞는지 Yes / No 만 판정 (여기서는 내용을 고칠 수 없음)
왜 반드시 Mutating(변형)이 Validating(검증)보다 먼저 실행될까요?

순서가 바뀌면 큰일이 나기 때문입니다! 만약 합격 도장(Validating)을 먼저 찍은 뒤에 누군가 몰래 도면을 개조(Mutating)해 버리면, 개조된 도면에 문법 오류나 보안 위반이 있어도 그대로 etcd에 저장되어 버립니다. 그래서 쿠버네티스는 언제나 "먼저 다 고치고(Mutating) → 고쳐진 최종본을 검사한다(Validating)" 는 철칙을 지킵니다.


1-2. istiod가 파드 설계도에 몰래 그려 넣는 2개의 컨테이너

우리가 Part 01에서 네임스페이스에 istio-injection=enabled 라는 라벨을 붙였을 때, 쿠버네티스의 3단계 심사대(MutatingAdmissionWebhook)는 다음과 같이 동작했습니다.

  1. "part1-demo 네임스페이스에 새 파드를 만들겠다는 요청이 들어왔네? 어라, 이 네임스페이스에는 istio-injection=enabled 스티커가 붙어 있잖아!"
  2. 그러자 kube-apiserver는 잠시 멈추고, istio-system에 떠 있는 istiod 파드의 웹훅 포트(443) 로 방금 받은 파드 YAML을 보내며 물어봅니다: "이 파드 설계도 어떻게 개조할까요?"
  3. istiod는 0.01초 만에 "원래 있던 컨테이너는 건드리지 말고, 그 옆에 아래 두 개의 컨테이너와 공유 볼륨들을 추가로 끼워 넣으세요(JSONPatch)!" 라고 답장을 보냅니다.

그 결과 내 파드 안에는 원래 내가 적은 앱 컨테이너 외에 두 명의 요원이 추가로 탑승하게 됩니다.

flowchart TB
    subgraph POD["개조 수술이 끝난 Pod 내부 (같은 IP · 같은 네트워크 공간 공유)"]
        direction TB
        INIT["👷‍♂️ 1. 초기화 컨테이너 (InitContainer: istio-init)
• 본 컨테이너들이 켜지기 전에 가장 먼저 단독 실행됨
• 커널에 iptables 납치 규칙(15006 / 15001)만 설치하고
• 1초 만에 임무 완료 후 스스로 종료됨 (Completed)"] subgraph MAIN["본 컨테이너들 (InitContainer가 성공해야 동시에 켜짐: READY 2/2)"] direction LR APP["🛵 2. 내 비즈니스 컨테이너
(study-client 또는 echo)
• 일반 유저(UID 100 / 1000)로 실행
• 아무것도 모른 채 평범하게 통신"] PROXY["🛡️ 3. 사이드카 컨테이너 (istio-proxy)
• 반드시 UID 1337 계정이 부여됨!
• pilot-agent + envoy 프로세스 실행
• 15006(입구) · 15001(출구) 포트 대기"] end INIT == "① 커널 네트워크 세팅 완료 후 바통 터치" ==> MAIN end

1-3. 왜 istio-init 이라는 '초기화 컨테이너(InitContainer)'가 따로 필요할까요?

여기서 중요한 의문이 생깁니다.

"그냥 istio-proxy(Envoy 사이드카) 컨테이너 하나만 넣어서, 걔가 켜질 때 커널 네트워크(iptables) 설정도 같이 하면 안 되나요? 왜 굳이 1초 만에 죽는 istio-init 컨테이너를 따로 먼저 돌리나요?"

두 가지 결정적인 이유가 있습니다!

  1. 첫째, 기동 순서 보장 (내 앱이 Envoy보다 먼저 패킷을 쏘는 사고 방지)
    • 쿠버네티스에서 일반 컨테이너 2개(내 앱 + istio-proxy)는 동시에 켜집니다. 누가 0.1초 먼저 켜질지 아무도 보장할 수 없습니다.
    • 만약 커널에 iptables 납치 규칙이 깔리기도 전에 내 앱이 먼저 켜져서 외부로 요청을 보내버리면, 그 첫 요청은 서비스 메시(mTLS 암호화 등)를 타지 않고 그냥 날아가 버립니다.
    • 반면 쿠버네티스의 초기화 컨테이너(initContainers) 는 "내가 완전히 실행을 마치고 성공(Completed)하기 전까지는 본 컨테이너(containers)들을 절대 깨우지 않는다" 는 100% 순서 보장 규칙을 가집니다. 따라서 istio-init이 먼저 커널 네트워크 공사를 완벽히 끝낸 뒤에야 내 앱과 Envoy가 켜집니다.
  2. 둘째, 최소 권한 원칙 (보안 분리)
    • 리눅스 커널의 네트워크 길목(iptables)을뜯어고치려면 리눅스 커널의 NET_ADMIN 과 NET_RAW 라는 막강한 네트워크 관리자 권한(Capability)이 필요합니다.
    • 만약 24시간 내내 외부 트래픽을 받는 istio-proxy(Envoy) 컨테이너에게 이 NET_ADMIN 권한을 계속 들려주면, 혹시라도 Envoy가 해킹당했을 때 해커가 네트워크 규칙을 마음대로 조작할 수 있습니다.
    • 그래서 istio-init에게만 잠깐 NET_ADMIN 권한을 주어 1초 만에 iptables 규칙만 딱 깔고 사라지게(종료되게) 만들고, 정작 24시간 돌아가는 istio-proxy는 아무런 커널 관리자 권한이 없는 평범한 일반 계정(UID 1337)으로만 돌리는 것입니다!
용어 정리

  • 어드미션 컨트롤러 (Admission Controller): kubectl 요청이 인증·인가를 통과한 직후, etcd에 저장되기 직전에 요청을 가로채서 내용을 변형(Mutating)하거나 최종 검사(Validating)하는 쿠버네티스 API 서버의 플러그인입니다.
  • MutatingAdmissionWebhook (변형 어드미션 웹훅): 오브젝트가 생성되기 직전에 외부 서버(예: istiod)를 호출해 YAML 명세를 실시간으로 수정·추가하는 기능입니다. Istio의 사이드카 자동 주입이 바로 이 기능을 사용합니다.
  • JSONPatch: YAML/JSON 문서 전체를 다시 보내는 대신, "containers 배열의 1번 칸에 istio-proxy 블록을 추가(add)하라" 처럼 바뀔 부분만 콕 집어서 알려주는 수정 지시서입니다.
  • 초기화 컨테이너 (InitContainer): 파드 안의 메인 컨테이너들이 시작되기 전에 무조건 먼저 실행되어 완료(Completed)되어야 하는 일회용 준비 컨테이너입니다.
  • 리눅스 Capabilities (NET_ADMIN, NET_RAW): 옛날 리눅스는 "무소불위의 신(root, UID 0)" 아니면 "아무 권한 없는 평민" 두 가지뿐이었습니다. 이 위험을 줄이기 위해 root의 권한을 수십 개의 작은 조각(능력, Capability)으로 쪼개어 필요한 조각만 줄 수 있게 한 기능입니다. 그중 NET_ADMIN 과 NET_RAW 는 방화벽(iptables)과 네트워크 인터페이스를 수정할 수 있는 권한입니다.
  • pilot-agent: istio-proxy 컨테이너 안에서 가장 먼저 실행되는 관리 프로그램(Go 언어)입니다. Envoy(C++ 프로그램)를 자식 프로세스로 실행해 감시하고, istiod로부터 인증서(SDS)를 받아 Envoy에게 건네주는 보좌관 역할을 합니다.


2. 미스터리 ② 해부: 리눅스 커널은 어떻게 패킷을 가로채나? (Netfilter와 iptables)

이제 istio-init이 파드의 리눅스 커널 안에 설치하고 간 패킷 납치 장치(iptables) 를 해부할 차례입니다.

2-1. 초보자를 위한 Netfilter와 iptables 1분 개념 정리

리눅스 운영체제 안으로 네트워크 패킷이 들어오거나 나갈 때는 반드시 커널 바닥에 있는 5개의 도로 검문소를 지나가야 합니다.

용어 비유 정확한 뜻
Netfilter (넷필터) 리눅스 커널 도로 곳곳에 지어진 검문소 건물 리눅스 커널 내부에 내장되어 지나가는 모든 네트워크 패킷을 붙잡아 확인·수정·차단하는 프레임워크
iptables (아이피테이블스) 검문소 경비원에게 건네주는 근무 수칙 표지판 사용자가 커널의 Netfilter 검문소에 "80번으로 가는 패킷이 오면 15001번으로 돌려보내라" 같은 규칙을 등록하고 조회하는 명령어 도구 (최신 커널에서는 내부적으로 nf_tables 엔진 위에서 돌아감)
nat 테이블 (주소 변환 표) 패킷의 출발지나 목적지 주소:포트를 고쳐 적는 장부 iptables에는 패킷을 차단하는 filter 테이블과 주소를 바꾸는 nat (Network Address Translation) 테이블이 있습니다. Istio는 패킷의 목적지 포트를 Envoy 포트로 바꿔치기해야 하므로 nat 테이블만 사용합니다!
REDIRECT 타겟 "원래 목적지 무시하고 내 로컬 포트로 꺾어라!" 패킷의 목적지 IP를 강제로 현재 파드 자신(127.0.0.1) 으로 바꾸고, 목적지 포트를 지정한 포트(15001 또는 15006)로 바꿔치기(DNAT)하는 명령
왜 내 파드의 iptables를 고쳐도 같은 서버(노드)에 있는 다른 파드에는 영향이 없을까요?

Docker/Kubernetes 기초에서 배운 네트워크 네임스페이스(netns) 덕분입니다!
리눅스 커널은 하나지만, 파드(Pod)마다 각자 완전히 독립된 가상 네트워크 방(netns) 을 하나씩 받습니다. 방마다 자기만의 랜카드(eth0, lo)와 자기만의 독립된 iptables 규칙 장부를 따로 가지고 있기 때문에, study-client 파드 안에서 iptables를 아무리 뜯어고쳐도 옆집 파드나 호스트 노드에는 털끝만큼도 영향을 주지 않습니다!


2-2. 패킷이 끌려가는 2개의 길목: 입구(PREROUTING)와 출구(OUTPUT)

파드의 네트워크 방(netns) 안에는 Istio가 사용하는 두 개의 핵심 길목(Chain)이 있습니다.

  1. PREROUTING (입구 검문소): 파드 바깥에서 랜선(eth0)을 타고 방금 파드 안으로 도착한 패킷이 내 앱 컨테이너에 닿기 직전에 무조건 거치는 첫 번째 관문입니다.
    • Istio는 여기에 ISTIO_INBOUND 라는 전용 검문소를 연결해 두고, 밖에서 들어오는 모든 TCP 패킷을 무조건 15006번 포트(Envoy의 virtualInbound 입구 창구) 로 꺾어 버립니다(REDIRECT --to-ports 15006).
  2. OUTPUT (출구 검문소): 파드 안의 내 프로그램(curl, Python, Java 등)이 바깥으로 보내려고 방금 만든 패킷이 랜선(eth0)으로 빠져나가기 직전에 거치는 출구 관문입니다.
    • Istio는 여기에 ISTIO_OUTPUT 이라는 전용 검문소를 연결해 두고, 안에서 밖으로 나가려는 모든 TCP 패킷을 무조건 15001번 포트(Envoy의 virtualOutbound 출구 창구) 로 꺾어 버립니다(REDIRECT --to-ports 15001).
flowchart LR
    subgraph POD_NETNS["Pod 네트워크 네임스페이스 (netns) 내부"]
        direction TB
        ETH0["🔌 eth0
(파드 외부 랜선)"] subgraph KERNEL["🐧 리눅스 커널 Netfilter (iptables -t nat)"] PRE["1️⃣ PREROUTING 체인
(밖에서 들어온 패킷)"] --> INB["ISTIO_INBOUND
→ REDIRECT :15006"] OUT["2️⃣ OUTPUT 체인
(안에서 나가는 패킷)"] --> OUTB["ISTIO_OUTPUT
→ REDIRECT :15001"] end subgraph ENVOY["🛡️ istio-proxy (Envoy, UID = 1337)"] P15006["입구 창구 :15006
(virtualInbound)"] P15001["출구 창구 :15001
(virtualOutbound)"] end APP["🛵 내 비즈니스 앱
(UID = 100 또는 1000)
포트 :8080"] end ETH0 -- "외부 요청 도착" --> PRE INB == "강제 납치!" ==> P15006 P15006 -- "검문 완료 후 전달
(출발지: 127.0.0.6)" --> APP APP -- "외부로 요청 발송
(예: order-api:80)" --> OUT OUTB == "강제 납치!" ==> P15001 P15001 -- "UID 1337 신분증으로
OUTPUT 검문 무사통과!" --> ETH0
잠깐! 목적지 주소가 127.0.0.1:15001로 덮어써지면, Envoy는 이 패킷이 원래 어디(order-api:80)로 가려던 패킷인지 어떻게 아나요?

정말 예리한 질문입니다! 만약 iptables가 패킷 봉투의 목적지를 127.0.0.1:15001로 바꿔치기하면서 원래 주소를 지워버렸다면 Envoy도 어디로 배달해야 할지 알 수 없습니다.
다행히 리눅스 커널의 연결 추적 장부(conntrack)는 목적지를 바꾸기 전의 원래 목적지 주소(10.96.x.x:80)를 메모리에 따로 적어 둡니다. 그리고 Envoy가 리눅스 커널에게 SO_ORIGINAL_DST 라는 특수 시스템 콜(질문 옵션)로 "이 연결의 원래 목적지가 어디였어?" 라고 물어보면, 커널이 "원래는 10.96.x.x:80(order-api)으로 가던 녀석이야!" 라고 알려줍니다. 그래서 Envoy가 원래 목적지를 정확히 알고 RDS → CDS → EDS로 라우팅할 수 있는 것입니다!


3. 무한 루프(Infinite Loop) 탈출의 두 가지 마법: UID 1337 과 127.0.0.6

위 그림을 가만히 보면 소름 끼치는 무한 루프(Infinite Loop) 함정이 두 군데 숨어 있습니다. 만약 이 함정을 피하는 규칙이 없다면 패킷은 파드 밖으로 한 발짝도 나가지 못하고 영원히 파드 안에서 빙글빙글 돌다가 죽어버립니다!


3-1. [함정 1 — 출구 무한 루프] 와 UID 1337 신분증 해법

먼저 내 앱(study-client)이 밖으로 패킷을 보낼 때를 생각해 봅시다.

  1. 내 앱(curl)이 order-api:80으로 패킷을 쏩니다.
  2. 커널의 OUTPUT 검문소가 이 패킷을 잡아서 같은 파드 안의 Envoy(15001번 포트) 에게 밀어 넣습니다.
  3. Envoy가 패킷을 받아서 L7 로드밸런싱과 mTLS 암호화를 마친 뒤, 이제 진짜로 바깥(10.244.2.3:8080)으로 패킷을 쏩니다.
  4. 🚨 위기 발생!: Envoy도 똑같이 이 파드 안에 살고 있는 프로세스입니다! 따라서 Envoy가 밖으로 쏜 패킷 역시 똑같이 커널의 OUTPUT 검문소를 지나갑니다!
  5. 만약 OUTPUT 검문소가 아무 생각 없이 Envoy가 쏜 패킷마저 또 잡아서 15001번(Envoy 자신)으로 돌려보내면? Envoy → OUTPUT → 15001(Envoy) → OUTPUT → 15001(Envoy)... 무한 루프에 빠져 버립니다!

💡 Istio의 해결책: --uid-owner 1337 -j RETURN

이 문제를 풀기 위해 Istio는 리눅스의 사용자 번호(UID, User ID) 를 이용합니다.

flowchart LR
    APP["🛵 내 앱 (UID = 100)
패킷 발송"] --> CHK{"🐧 ISTIO_OUTPUT 검문소
'누가 보낸 패킷인가?'
(--uid-owner 1337)"} CHK -- "UID != 1337
(일반 앱이 보냄)" --> RED["🚨 ISTIO_REDIRECT
15001번(Envoy)으로 납치!"] RED --> ENV["🛡️ Envoy (UID = 1337)
L7 라우팅 · mTLS 암호화 후
다시 바깥으로 패킷 발송"] ENV --> CHK CHK == "UID == 1337!
(Envoy 요원이 보냄)" ==> PASS["✅ RETURN (검문 통과!)
eth0 랜선 타고 외부 파드로 출발!"]

3-2. [함정 2 — 입구 무한 루프] 와 127.0.0.6 루프백 마법

이번에는 반대로 외부에서 내 파드(order-api, 8080번 포트)로 요청이 들어올 때의 함정을 보겠습니다.

  1. 외부 파드에서 order-api 파드(10.244.2.3:8080)로 패킷이 도착합니다.
  2. 입구 검문소(PREROUTING → ISTIO_INBOUND)가 이 패킷을 낚아채서 Envoy의 입구 창구(15006번 포트) 로 넣습니다.
  3. Envoy(UID 1337)가 암호(mTLS)를 풀고 신분증을 검사한 뒤, 이제 같은 파드 안에 있는 진짜 order-api 앱 컨테이너(8080번 포트) 에게 패킷을 토스하려고 합니다.
  4. 🚨 위기 발생!: 같은 파드 안의 컨테이너끼리 통신할 때는 로컬 루프백(lo)을 타고 패킷이 이동하는데, 로컬에서 만든 패킷도 커널의 OUTPUT → ISTIO_OUTPUT 검문소를 지나갑니다!
    • 그런데 ISTIO_OUTPUT에는 "파드 안의 프로세스가 자기 자신의 파드 IP(10.244.2.3:8080)를 부를 때도 입구 검문(15006)을 거치게 하라" 는 규칙(! -d 127.0.0.1/32 -o lo ... --uid-owner 1337 -j ISTIO_IN_REDIRECT)이 들어 있습니다.
    • 만약 그냥 보내면, Envoy(15006)가 내 앱(8080)에게 토스한 패킷이 다시 15006(Envoy 자신)으로 끌려 들어가는 입구 무한 루프가 터집니다!

💡 Istio의 해결책: 출발지 IP를 127.0.0.6 으로 찍어서 보내기!

여기서 리눅스 네트워크의 아주 재미있는 성질 하나가 쓰입니다.


3-3. 파드의 iptables -t nat 전체 규칙 한 줄씩 완벽 해독표

잠시 뒤 Part 02 실습에서 직접 출력해 볼 실제 iptables 규칙 전체를 한 줄도 빠짐없이 우리말로 번역한 해독표입니다. 이 표 하나면 전 세계 어떤 Istio 사이드카의 커널 규칙도 완벽히 읽을 수 있습니다!

① 입구 규칙 (PREROUTING → ISTIO_INBOUND → ISTIO_IN_REDIRECT)

순서 실제 커널 iptables 규칙 무슨 뜻인가요? (완벽 번역)
1 -A PREROUTING -p tcp -j ISTIO_INBOUND 밖에서 들어오는 모든 TCP 패킷은 일단 ISTIO_INBOUND 검문소로 보내라!
2 -A ISTIO_INBOUND -p tcp --dport 15008 -j RETURN 목적지가 15008번(Ambient HBONE 터널 포트)이면 납치하지 말고 그대로 통과(RETURN)!
3 -A ISTIO_INBOUND -p tcp --dport 15090 -j RETURN 목적지가 15090번(Envoy 자신의 Prometheus 메트릭 포트)이면 그대로 통과!
4 -A ISTIO_INBOUND -p tcp --dport 15021 -j RETURN 목적지가 15021번(K8s kubelet이 찌르는 헬스체크 포트)이면 그대로 통과!
5 -A ISTIO_INBOUND -p tcp --dport 15020 -j RETURN 목적지가 15020번(pilot-agent 통합 메트릭/디버그 포트)이면 그대로 통과!
6 -A ISTIO_INBOUND -p tcp -j ISTIO_IN_REDIRECT 위 4개 관리 포트가 아닌 나머지 모든 들어오는 비즈니스 트래픽(예: 8080) 은 ISTIO_IN_REDIRECT로 보내라!
7 -A ISTIO_IN_REDIRECT -p tcp -j REDIRECT --to-ports 15006 들어온 패킷의 목적지를 Envoy 입구 창구인 15006번 포트로 강제 전환하라!

② 출구 규칙 (OUTPUT → ISTIO_OUTPUT → ISTIO_REDIRECT)

순서 실제 커널 iptables 규칙 무슨 뜻인가요? (완벽 번역)
1 -A OUTPUT -j ISTIO_OUTPUT 파드 안에서 출발하는 모든 패킷은 일단 ISTIO_OUTPUT 검문소로 보내라!
2 -A ISTIO_OUTPUT -s 127.0.0.6/32 -o lo -j RETURN [입구 루프 방지] 출발지가 127.0.0.6 이면 Envoy(15006)가 검문을 마치고 내 앱(8080)에게 토스하는 패킷이니 즉시 통과!
3 -A ISTIO_OUTPUT ! -d 127.0.0.1/32 -o lo -p tcp ! --dport 15008 -m owner --uid-owner 1337 -j ISTIO_IN_REDIRECT Envoy(UID 1337)가 자기 자신의 파드 IP로 호출할 때는 입구 창구(15006)로 보내라!
4 -A ISTIO_OUTPUT -o lo -m owner ! --uid-owner 1337 -j RETURN 내 일반 앱(UID != 1337)이 같은 파드 안의 localhost끼리 통신할 때는 가로채지 말고 그대로 통과!
5 -A ISTIO_OUTPUT -m owner --uid-owner 1337 -j RETURN [출구 루프 방지 핵심!] UID 1337(Envoy) 이 밖으로 내보내는 패킷은 다시 납치하지 말고 즉시 바깥 네트워크로 통과시켜라!
6~8 (... --gid-owner 1337 관련 3줄) 위 3~5번 규칙을 그룹 번호(GID 1337) 기준으로도 똑같이 한 번 더 보호!
9 -A ISTIO_OUTPUT -d 127.0.0.1/32 -j RETURN 목적지가 127.0.0.1인 로컬 통신은 가로채지 말고 통과!
10 -A ISTIO_OUTPUT -j ISTIO_REDIRECT 위 예외에 해당하지 않는 내 비즈니스 앱이 밖으로 보낸 모든 패킷은 ISTIO_REDIRECT로 보내라!
11 -A ISTIO_REDIRECT -p tcp -j REDIRECT --to-ports 15001 나가는 패킷의 목적지를 Envoy 출구 창구인 15001번 포트로 강제 전환하라!
용어 정리

  • 네트워크 네임스페이스 (netns, Network Namespace): 리눅스 커널 하나 위에서 컨테이너나 파드마다 독립된 가상 랜카드(eth0, lo), 라우팅 테이블, 방화벽(iptables) 규칙을 갖도록 격리해 주는 공간입니다. 같은 Pod 안의 컨테이너들(내 앱, istio-init, istio-proxy)은 단 하나의 netns를 함께 공유합니다.
  • RETURN 타겟 (-j RETURN): iptables에서 "이 패킷은 더 이상 아래쪽 납치(REDIRECT) 규칙들을 검사하지 말고, 지금 즉시 이 검문소 체인을 빠져나가 원래 가던 길로 가라!"는 탈출 명령입니다.
  • UID 1337 (istio-proxy): 파드 안에서 Envoy 사이드카 프로세스가 실행될 때 부여받는 고유한 리눅스 사용자 ID입니다. 커널 iptables는 패킷을 만든 소켓의 주인이 UID 1337인지 확인하여 Envoy가 밖으로 내보내는 패킷이 다시 Envoy로 빨려 들어가는 무한 루프를 막습니다.
  • 127.0.0.6 (Inbound Passthrough Bind IP): Envoy 입구(15006)가 외부 요청을 받아 복호화한 뒤 같은 파드 안의 비즈니스 컨테이너로 전달할 때 사용하는 특수 출발지 루프백 IP입니다.


4. 1세대 사이드카(Sidecar) 메시의 구조적 한계 — 왜 2세대 Ambient Mesh가 필요해졌나?

지금까지 살펴본 1세대 사이드카 아키텍처는 정말 정교하고 기발한 설계입니다. 하지만 전 세계 기업들이 파드 수백~수천 개 규모로 이 구조를 수년간 운영해 보니 4가지 뼈아픈 한계가 드러났습니다.

flowchart TB
    subgraph LIMIT["⚠️ 1세대 Sidecar 메시의 4대 운영 한계 (Part 03 Ambient Mesh의 탄생 배경)"]
        direction LR
        L1["1️⃣ 리소스 중복 낭비
파드 1,000개면 Envoy도 1,000개!
각 Envoy마다 전체 클러스터 xDS 지도를
중복 보유해 메모리 수십 GB 소모"] L2["2️⃣ 라이프사이클 강결합
Envoy 보안 패치 하나만 하려 해도
전사 비즈니스 파드 1,000개를
전부 롤링 재시작해야 함!"] L3["3️⃣ 기동/종료 순서 꼬임 (Race Condition)
Job/배치 파드가 할 일 끝내고 종료돼도
istio-proxy가 안 꺼져서 파드가
NotReady/Running에 걸리는 현상"] L4["4️⃣ 과도한 L7 파싱 비용
단순히 암호화(mTLS)만 필요한 파드도
무조건 무거운 L7 Envoy 전체를
사이드카로 띄워야 함"] end
  1. 메모리와 CPU의 O(N) 중복 낭비: 파드가 500개 떠 있으면 똑같은 Envoy 프로세스도 500개가 뜹니다. 더 큰 문제는 500개의 Envoy가 저마다 클러스터 전체의 xDS 지도 사본을 자기 메모리에 들고 있어서 메모리 사용량이 기하급수적으로 늘어납니다.
  2. 프록시 업그레이드 = 비즈니스 앱 전체 재배포: 사이드카는 내 비즈니스 파드 안에 한 몸으로 들어 있습니다. 따라서 Istio(Envoy) 버전을 1.31.2에서 1.31.3으로 올리려면, 멀쩡히 잘 돌고 있는 비즈니스 파드 수백 개를 전부 강제로 재시작(rollout restart) 해야 합니다.
  3. 배치 작업(Job / CronJob)과의 충돌: 5분 동안 데이터를 집계하고 꺼져야 하는 쿠버네티스 Job 파드에 사이드카가 붙으면, 내 배치 컨테이너는 성공(Completed)하고 꺼졌는데 옆자리의 istio-proxy 컨테이너는 혼자 계속 살아 있어서(READY 1/2) Job이 영원히 끝나지 않는 유명한 골칫거리가 발생합니다.
  4. 불필요한 L7 비용 강제: 단순히 "파드끼리 통신할 때 도청만 막자(L4 mTLS 암호화)"가 목적인데도, 무조건 무거운 L7 Envoy 프록시를 모든 파드에 다 넣어야 했습니다.
이 4가지 한계를 단번에 깨부순 것이 바로 다음 파트(Part 03)의 주인공인 2세대 Ambient Mesh(ztunnel + Waypoint) 입니다!

하지만 오늘 배운 1세대 커널 해부를 정확히 알아야만, Part 03에서 왜 Ambient Mesh가 혁신적인지, 그리고 L7 Waypoint 프록시가 내부적으로 어떻게 동작하는지 100% 이해할 수 있습니다.


핵심 요약

Part 02 핵심 정리
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
① 사이드카 주입   kubectl apply 시 kube-apiserver의 3단계 검문소(MutatingAdmissionWebhook)가
                 istiod를 호출해 YAML에 'istio-init'과 'istio-proxy'를 자동으로 끼워 넣음.
② istio-init     본 컨테이너보다 먼저 실행되는 초기화 컨테이너(InitContainer).
                 NET_ADMIN 권한으로 파드 netns에 iptables NAT 규칙만 깔고 1초 만에 종료됨(최소 권한 원칙).
③ 패킷 납치 포트  • 밖에서 들어오는 패킷: PREROUTING → ISTIO_INBOUND → 15006번(virtualInbound)
                 • 안에서 나가는 패킷:   OUTPUT     → ISTIO_OUTPUT  → 15001번(virtualOutbound)
                 • 원래 목적지는 커널 conntrack에 보관되며 Envoy가 SO_ORIGINAL_DST로 조회!
④ 무한 루프 방지  • 출구 루프 방지: Envoy는 'UID 1337'로 실행되며, --uid-owner 1337 규칙으로 즉시 RETURN!
                 • 입구 루프 방지: Envoy(15006)가 내 앱(8080)에 토스할 때는 출발지를 '127.0.0.6'으로 찍어 RETURN!
⑤ 1세대의 한계    파드마다 Envoy가 뜨는 메모리 중복·앱 재시작 강제·Job 종료 안 됨 문제
                 → 이를 해결하기 위해 사이드카 없는 2세대 Ambient Mesh(Part 03)가 탄생!

참고 자료

공식 문서:


네비게이션

이전 목록 다음
◀ Part 01 실습 목록으로 Part 02 실습 ▶